package yawning.mew.character;

import java.net.URI;
import java.net.URL;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

import yawning.mew.equipment.ArmoryItemSource;
//import yawning.mew.equipment.DatabaseItemSource;
import yawning.mew.equipment.Enchant;
import yawning.mew.equipment.EquipSet;
import yawning.mew.equipment.EquipSet.Slot;
import yawning.mew.equipment.Item;
import yawning.mew.equipment.ItemSource;
import yawning.mew.equipment.MemoryCacheItemSource;
import yawning.mew.equipment.RawItem;
import yawning.mew.equipment.RawItem.Property;
import yawning.mew.equipment.WowheadItemSource;
import yawning.mew.equipment.XMLItemSource;
import yawning.mew.util.BattleNetAuth;
import yawning.mew.util.URLFetcher;

public class ArmoryToonBuilder extends ExternalToonBuilder {
	public enum TalentSpec {
		ACTIVE,
		PRIMARY,
		SECONDARY
	}

	private String mName;
	private String mRegion;
	private String mServer;
	private EquipSet mEquipSet;
	private Toon mToon;
	private int mArmoryAttackPower;
	private TalentSpec mTalentSpec = TalentSpec.ACTIVE;


	public ArmoryToonBuilder() {
		super();
	}

	public ArmoryToonBuilder setName(String name) {
		mName = name;
		return this;
	}

	public ArmoryToonBuilder setRegion(String region) {
		mRegion = region;
		return this;
	}

	public ArmoryToonBuilder setServer(String server) {
		mServer = server;
		return this;
	}

	public ArmoryToonBuilder setTalentSpec(TalentSpec spec) {
		mTalentSpec = spec;
		return this;
	}

	@Override
	public EquipSet getEquipSet() {
		return mEquipSet;
	}

	@Override
	public int getCharacterSheetAttackPower() {
		return mArmoryAttackPower;
	}

	@Override
	public int getCharacterSheetStamina() {
		// FIXME: This is where I would do the right thing and derive the proper value, but mToon has the character sheet value already.
		return mToon.stamina;
	}

	@Override
	public int getCharacterSheetParryRating() {
		// FIXME: This is where I would do the right thing and derive the proper value, but mToon has the character sheet value already.
		return mToon.mParryRating;
	}

	@Override
	public int getCharacterSheetArmor() {
		// FIXME: This is where I would do the right thing and derive the proper value, but mToon has the character sheet value already.
		return mToon.armor;
	}

	@Override
	public void addProgressCallback(ToonBuilderProgressCallback fn) {
		mProgressCallbacks.add(fn);
	}

	@Override
	public Toon build() throws ToonBuildException {
		try {
			//
			// You can get nice JSON formated data from battle.net.
			//
			// Eg: http://us.battle.net/api/wow/character/doomhammer/reesi?fields=items,talents,stats,professions
			if(!onProgress("Fetching character data from the Armory")) return null;

			URI uri = new URI("http", null, mRegion + ".battle.net", 80, "/api/wow/character/" + mServer + "/" + mName, "fields=items,stats,talents,professions&locale=en", null);
			URL url = new URL(uri.toASCIIString());
//			System.out.println(url.toString());

			// Optionally use authentication
			Map<String,String> reqProps = null;
			BattleNetAuth auth = BattleNetAuth.getInstance();
			if (auth.hasKeys()) {
				reqProps = auth.buildAuthRequestProps(uri);
			}

			String page = URLFetcher.getInstance().fetchAsString(url, false, reqProps);

			JSONObject jso = (JSONObject)JSONValue.parse(page);
			JSONObject jso2, jso3;
			JSONArray jsa;
//			System.out.println(jso);

			Toon t = new Toon();

			t.mClass = Clazz.get(((Number) jso.get("class")).intValue());
			t.level = ((Number) jso.get("level")).intValue();
			t.mRace = Race.get(((Number) jso.get("race")).intValue());

			// Handle talents/glyphs
			jsa = (JSONArray) jso.get("talents");
			if (mTalentSpec == TalentSpec.PRIMARY) {
				jso2 = (JSONObject) jsa.get(0);
			} else if (mTalentSpec == TalentSpec.SECONDARY) {
				jso2 = (JSONObject) jsa.get(1);
			} else {
				jso2 = (JSONObject) jsa.get(0);
				if (!jso2.containsKey("selected")) {
					jso2 = (JSONObject) jsa.get(1);
				}
			}
			t.mTalentSpec = TalentTree.get(t.mClass, (String) jso2.get("name"));
			t.mTalents = ((String) jso2.get("build")).getBytes();
			for (int j = 0; j < t.mTalents.length; j++) t.mTalents[j] -= '0';

			jso3 = (JSONObject) jso2.get("glyphs");
			jsa = (JSONArray) jso3.get("prime");
			for (int j = 0; j < jsa.size(); j++) {
				Glyph g = Glyph.getByName((String) ((JSONObject) jsa.get(j)).get("name"));
				if (g != null) t.mGlyphs.add(g);
			}
			jsa = (JSONArray) jso3.get("major");
			for (int j = 0; j < jsa.size(); j++) {
				Glyph g = Glyph.getByName((String) ((JSONObject) jsa.get(j)).get("name"));
				if (g != null) t.mGlyphs.add(g);
			}
			jsa = (JSONArray) jso3.get("minor");
			for (int j = 0; j < jsa.size(); j++) {
				Glyph g = Glyph.getByName((String) ((JSONObject) jsa.get(j)).get("name"));
				if (g != null) t.mGlyphs.add(g);
			}

			jso2 = (JSONObject) jso.get("professions");
			jsa = (JSONArray) jso2.get("primary");
			for (int j = 0; j < jsa.size(); j++) {
				jso3 = (JSONObject) jsa.get(j);
				t.addProfession(Profession.get(((Number)jso3.get("id")).intValue()), ((Number)jso3.get("rank")).intValue());
			}

			// Handle statistics
			jso2 = (JSONObject) jso.get("stats");
			t.strength = ((Number)jso2.get("str")).intValue();
			t.agility = ((Number)jso2.get("agi")).intValue();
			t.stamina = ((Number)jso2.get("sta")).intValue();
			t.intellect = ((Number)jso2.get("int")).intValue();
			t.spirit = ((Number)jso2.get("spr")).intValue();
			t.armor = ((Number)jso2.get("armor")).intValue();
			t.bonusArmor = 0; // TODO: Figure this out without the nastiness.

			t.mDodgeRating = ((Number)jso2.get("dodgeRating")).intValue();
			t.mParryRating = ((Number)jso2.get("parryRating")).intValue();
			t.hasteRating = ((Number)jso2.get("hasteRating")).intValue();
			t.critRating = ((Number)jso2.get("critRating")).intValue();
			t.hitRating = ((Number)jso2.get("hitRating")).intValue();
			t.expertiseRating = ((Number)jso2.get("expertiseRating")).intValue();
			t.masteryRating = ((Number)jso2.get("masteryRating")).intValue();
			t.mResilienceRating = ((Number)jso2.get("resil")).intValue();

			// TODO: Sigh, this JSON thing doesn't provide any convenient way of determining
			// pre-multiplier attack power, see the second armory query below for how I work around this.
			mArmoryAttackPower = ((Number)jso2.get("attackPower")).intValue();

			// Remove haste and bonus AP damage from the json values for weapon speed and damage
			// Note that this has slight rounding errors, but we can query the item database for the real value later.
			StatLogic sl = StatLogic.get(t.level, t.mClass);
			Double hasteMult = 1.0 + t.hasteRating / sl.getHasteConv() / 100;
			double weaponSpeed = (Double)jso2.get("mainHandSpeed") * hasteMult;
			t.weaponSpeed = (int)(weaponSpeed * 1000);
			t.weaponTop = (int)((Double)jso2.get("mainHandDmgMax") - (mArmoryAttackPower) * weaponSpeed / 14.0 + 0.5);
			t.weaponBottom = (int)((Double)jso2.get("mainHandDmgMin") - (mArmoryAttackPower) * weaponSpeed / 14.0 + 0.5);

			// Handle gear
			mEquipSet = new EquipSet();
			jso2 = (JSONObject)jso.get("items");
			for (Slot slot : Slot.values()) {
				jso3 = (JSONObject)jso2.get(slot.toString());
				if (jso3 != null) {
					if (!onProgress("Fetching item information for: " + jso3.get("name"))) return null;

					Item item = new Item();
					item.mItem = ItemSource.getItem(((Number)jso3.get("id")).intValue(), true);

					JSONObject tooltipParams = (JSONObject)jso3.get("tooltipParams");
					if (tooltipParams != null) {
						if (tooltipParams.containsKey("reforge")) reforgeItem(item, ((Number)tooltipParams.get("reforge")).intValue());
						if (tooltipParams.containsKey("gem0")) item.addGem(ItemSource.getItem((int)((Number)tooltipParams.get("gem0")).intValue(), true), 0);
						if (tooltipParams.containsKey("gem1")) item.addGem(ItemSource.getItem((int)((Number)tooltipParams.get("gem1")).intValue(), true), 1);
						if (tooltipParams.containsKey("gem2")) item.addGem(ItemSource.getItem((int)((Number)tooltipParams.get("gem2")).intValue(), true), 2);
						if (tooltipParams.containsKey("suffix")) item.addSuffix((int)((Number)tooltipParams.get("suffix")).intValue());

						if (tooltipParams.containsKey("enchant")) item.addEnchant(Enchant.getEnchantByEnchantId((int)((Number)tooltipParams.get("enchant")).intValue(), true));
						if (tooltipParams.containsKey("tinker")) item.mTinker = (Enchant.getEnchantByEnchantId((int)((Number)tooltipParams.get("tinker")).intValue(), true));
					}

					mEquipSet.addItem(slot, item);
				}
			}

			t.isArmorSpec = (t.level >= 50) && mEquipSet.checkArmorSpec(t.mClass.mArmorSubclass);

			// Determine meta gem.
			Item hat = mEquipSet.mEquipmentMap.get(EquipSet.Slot.HEAD);
			if (hat != null) {
				RawItem metaSocket = hat.mSocket1;
				if (metaSocket != null) {
					if (metaSocket.name != null) {
						try {
							// Is it ok to assume that we can sort metagems by prefix?
							t.mMetaGem = MetaGem.valueOf(metaSocket.name.split(" ")[0].toUpperCase());
						} catch (IllegalArgumentException e) {
							t.mMetaGem = null;
						}
					} else {
						t.mMetaGem = MetaGem.get(metaSocket.id);
					}
				}
			}

			// The way we derive weapon speed/damage is susceptible to rounding errors, pull it directly from the item
			// if we have the information available.
			//
			// Note: Naturally Wowhead and the armory disagree about some item's weapon damage (See Malevolence), but
			// that will be solved when I write ArmoryItemSource.  At most in either case with or without the code
			// this is only going to be off by +- 1 DPS or so, but people are wankers and will complain.
			Item mh = mEquipSet.mEquipmentMap.get(EquipSet.Slot.MAIN_HAND);
			if (mh != null && mh.mItem.properties.containsKey(RawItem.Property.MELEE_DMG_MIN)
					&& mh.mItem.properties.containsKey(RawItem.Property.MELEE_DMG_MAX)
					&& mh.mItem.properties.containsKey(RawItem.Property.MELEE_DMG_MIN)) {
				t.weaponBottom = mh.mItem.properties.get(RawItem.Property.MELEE_DMG_MIN).intValue();
				t.weaponTop = mh.mItem.properties.get(RawItem.Property.MELEE_DMG_MAX).intValue();
				t.weaponSpeed = (int) Math.round(mh.mItem.properties.get(Property.MELEE_SPEED).doubleValue() * 1000);
			}

			//
			// HACK HACK HACK: Sigh, this new API is missing some things we need.  Screen scrape the armory for the rest.
			//
			// Note: This is rather fragile, however since all I'm doing is pulling stuff out of a JSON block embedded
			// in the xhtml response it shouldn't be *too* bad.
			//
			if (!onProgress("Fetching additional character data from the Armory (Scraping)")) return null;

			uri = new URI("http", null, mRegion + ".battle.net", 80, "/wow/en/character/" + mServer + "/" + mName + "/advanced", null, null);
			url = new URL(uri.toASCIIString());
			page = URLFetcher.getInstance().fetchAsString(url);

			Matcher m = Pattern.compile("new Summary\\.Stats\\((.+?)\\);", Pattern.DOTALL).matcher(page);
			m.find();
			jso = (JSONObject)JSONValue.parse(m.group(1));
//			System.out.println(jso);

			// All that work for one value that I end up actually using.
			t.bonusArmor = ((Number)jso.get("armorTotal")).intValue() - ((Number)jso.get("armorBase")).intValue();

			// Determine if AP has been multiplied, then remove the multiplier, and agi/strength contributed AP.
			// Note: Technically we don't need this at all right now since MewPivot uses character sheet/armory AP.
			int strAp = ((Number)jso.get("str_ap")).intValue();
			strAp = strAp > 0 ? strAp : 0;
			int agiAp = ((Number)jso.get("agi_ap")).intValue();
			agiAp = agiAp > 0 ? agiAp : 0;
			int baseAp = ((Number)jso.get("atkPowerBase")).intValue();
			double apMult = (double) baseAp / (strAp + agiAp + sl.getBaseAttackPower());
			int bonusAp = ((Number)jso.get("atkPowerBonus")).intValue();
			t.attackPower = (int)(bonusAp / apMult + 0.5) + sl.getBaseAttackPower();

			// FIXME: Technically speaking the value of armor in Toon is incorrect.  It's not actually used though, so it doesn't matter.
			mToon = t;

			return t;
		} catch (Exception e) {
			throw new ToonBuildException(e);
		}
	}

	private boolean onProgress(String msg) {
		boolean shouldContinue = true;
		for (ToonBuilderProgressCallback fn : mProgressCallbacks) {
			shouldContinue &= fn.onProgress(msg);
		}

		return shouldContinue;
	}

	private void reforgeItem(Item item, int code) {
		int from = (code - 113) / 7;
		int to = (code - 113) % 7;
		if (to >= from) to++;
		item.reforge(sReforge[from], sReforge[to]);
	}

	private static final Property[] sReforge = new Property[] {
			Property.SPIRIT,
			Property.DODGE_RATING,
			Property.PARRY_RATING,
			Property.HIT_RATING,
			Property.CRIT_RATING,
			Property.HASTE_RATING,
			Property.EXPERTISE_RATING,
			Property.MASTERY_RATING,
	};

	public static void main(String[] args) {
		ItemSource.pushBack(new MemoryCacheItemSource());
//		ItemSource.pushBack(new ItemSource() {
//			@Override
//			protected RawItem getById(int id) {
//				return new RawItem(id, null);
//			}
//		});
//		ItemSource.pushBack(new DatabaseItemSource());
		ItemSource.pushBack(new XMLItemSource());
		ItemSource.pushBack(new ArmoryItemSource());
		ItemSource.pushBack(new WowheadItemSource());

		// Optionally try to load the public/private key pair for authentication.
		BattleNetAuth auth = BattleNetAuth.getInstance();
		auth.loadDefaultKeys();

		try {
			ArmoryToonBuilder builder = new ArmoryToonBuilder()
					.setName("Tangedyn")
					.setRegion("us")
					.setServer("Thaurissan");
			Toon toon = builder.build();
			EquipSet equipSet = builder.getEquipSet();

			System.out.println(toon);
			System.out.println(equipSet);
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}
